package no.ffi.gismoidm.utils;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.PublicKey;
import java.util.Date;
import java.util.Hashtable;
import java.util.UUID;
import no.ffi.gismoidm.attrex.BooleanOperand;
import no.ffi.gismoidm.attrex.Parser;
import no.ffi.gismoidm.discovery.ServiceItem;
import no.ffi.gismoidm.idp.Config;
import no.ffi.gismoidm.invocations.ServiceInvocation;
import no.ffi.gismoidm.invocations.ServiceInvocationFactory;

import no.ffi.tpm.crypto.CryptoOperations;
import no.ffi.tpm.crypto.CryptoOperationsFactory;


/**
 *
 * @author anders
 */
public class ClientProxy {

    private CryptoOperations crOp;
    private PublicKey trustAnchorPublicKey;
    private Hashtable identityStatements, clockCounters;
    private final int MIN_MARGIN_TO_EXPIRATION = 14;
    
    public ClientProxy(String configFilename) throws Exception {
        if (configFilename != null && !configFilename.equals(""))
            System.setProperty("GISMOIDM.CONFIG", configFilename);
        Config.loadPropertiesFrom(null);
        String crEng = Config.getProperty("CRYPTO_ENGINE", "JCE"); // or "TPM"
        crOp = CryptoOperationsFactory.getInstance(crEng); 
        trustAnchorPublicKey = crOp.getTrustAnchor();
        identityStatements = new Hashtable();
        clockCounters = new Hashtable();
    }
    
    // This constructos is called if the config file is already read and
    // the cryptoOperations instance is created
    public ClientProxy(CryptoOperations crOp) {
        this.crOp = crOp;
        trustAnchorPublicKey = crOp.getTrustAnchor();
        identityStatements = new Hashtable();
        clockCounters = new Hashtable();        
    }

    public static IdentityStatement getIdentityStatement(Serializable request, String idpURL, CryptoOperations crOp) throws GismoIdmException {
        Object f=null;
        try {
            f = goHTTP(idpURL,request);
        } catch (Exception e) { throw new GismoIdmException(e.getMessage());}
        
        // The response object may need to be decrypted
        if (f instanceof EncryptedIdentityStatement) {
            try {
                EncryptedIdentityStatement encIs = (EncryptedIdentityStatement)f;
                IdentityStatement fa = encIs.decrypt(crOp);
                return fa;
            } catch (Exception e) { e.printStackTrace(); }
        }    
        else if (f instanceof IdentityStatement) {
            IdentityStatement fa = (IdentityStatement)f;
            return fa;
            
        } else if (f instanceof GismoIdmException) { // or a subclass exception
            throw (GismoIdmException)f;
        } else if (f instanceof Exception) { // or a subclass exception
            throw (new GismoIdmException(((Exception)f).getMessage()));
        } return null; // dummy
    }

    private IdentityStatement getLocalIdentityStatement() throws GismoIdmException {
        System.out.println("Fetch local IS");
        String idpURL = Config.getProperty("LOCAL_IDP_URL");
        Serializable request = null;
        try { // IS request may be a TPM-based key or a DN
            request = crOp.getISrequest();     
        } catch (Exception e) { throw new GismoIdmException(e.getMessage()); }
        
        IdentityStatement is = getIdentityStatement(request,idpURL,crOp);
        is.validateIdentityStatement(trustAnchorPublicKey);
        long clockCounter = is.getIssuingTimestamp() - System.currentTimeMillis();
        clockCounters.put(idpURL, clockCounter);
        identityStatements.put(idpURL, is);
        return is;
    }
    
    private IdentityStatement getGuestIdentityStatement(String xIdpURL, IdentityStatement localIS) throws GismoIdmException {
        System.out.println("Fetch guest IS for " + xIdpURL);
        if (xIdpURL == null) return null;
        IdentityStatement is = getIdentityStatement(localIS,xIdpURL,crOp);
        is.validateIdentityStatement(trustAnchorPublicKey);
        long clockCounter = is.getIssuingTimestamp() - System.currentTimeMillis();
        clockCounters.put(xIdpURL, clockCounter);
        identityStatements.put(xIdpURL, is);
        return is;
    }
    
    /* This method return the status of the cached identity statements. If the return value is
     * true, the next call to "invokeApplicationService" will retrieve a new set of identity
     * statements.
     * 
     * This method is designed to assist the service announcement process, which is why it does
     * not accept a service item, only description from config file
     */
    public boolean areCredentialsExpired(String appPrefix) {
        IdentityStatement guestIdentityStatement, localIdentityStatement;
        String xIdpURL = Config.getProperty(appPrefix + "_IDP_URL");
        String localIdpURL = Config.getProperty("LOCAL_IDP_URL");
        // If the idp URL of the service is the same is ours, then this is not
        // a cross COI operation, and the URL can be nullified
        if (localIdpURL.equals(xIdpURL)) xIdpURL = null; 
        localIdentityStatement = (IdentityStatement)identityStatements.get(localIdpURL);

        // If no special idp url is defined, then this is a local COI service
        if (xIdpURL==null) { 
            guestIdentityStatement = null;
        } else {
            guestIdentityStatement = (IdentityStatement)identityStatements.get(xIdpURL);
        }
                
        return (localIdentityStatement==null || 
               localIdentityStatement.identityStatementExpired(MIN_MARGIN_TO_EXPIRATION) || // TODO Or a max skew variable?
               (guestIdentityStatement != null && guestIdentityStatement.identityStatementExpired(0)));
    }


//    IdentityStatement crossAss = null;
    public Serializable invokeApplicationService(String appPrefix, Serializable request, String accessRequirements) throws Exception {
        return invokeApplicationService(appPrefix, null, request, accessRequirements);
    }

    public Serializable invokeApplicationService(ServiceItem serviceItem, Serializable request, String accessRequirements) throws Exception {
        return invokeApplicationService("", serviceItem, request, accessRequirements);
    }

    private Serializable invokeApplicationService(String appPrefix, ServiceItem serviceItem, Serializable request, String accessRequirements) throws Exception {
        IdentityStatement localIdentityStatement, guestIdentityStatement, tempIS;
        long clockCounter;
        
        // We can invoke services either based on information in the config file, or
        // based on information in the discovery information
        // Get info from config file on xIdP, serviceURL, serviceId and authentication protocol
        String localIdpURL = Config.getProperty("LOCAL_IDP_URL");
        String xIdpURL = Config.getProperty(appPrefix + "_IDP_URL",localIdpURL);
        String serviceURL = Config.getProperty(appPrefix + "_SERVICE_URL");
        String serviceID  = Config.getProperty(appPrefix + "_SERVICE_ID");
        String protocolClass = Config.getProperty(appPrefix + "_PROTOCOL_CLASS","STATEFUL");
        boolean allowExpiredIS = Config.getProperty(appPrefix + "_ACCEPTS_EXPIRED_IS") != null;       
        if (serviceItem != null) {
            // Deny the use of an expired serviceItem
            Date now = new Date();
            if (serviceItem.serviceExpiration.before(now))
                throw new ServiceInfoExpiredException("Service Item has expired, get new discovery information");
            serviceURL = serviceItem.serviceURL;
            serviceID = serviceItem.serviceDN;
            xIdpURL = serviceItem.idpURL;
            if (serviceItem.protocolClass==ServiceInvocationFactory.STATEFUL) protocolClass = "STATEFUL";
            else if (serviceItem.protocolClass==ServiceInvocationFactory.STATELESS) protocolClass = "STATELESS";
        }

        BooleanOperand clientReqs = Parser.parse(accessRequirements);
        // If the idp URL of the service is the same is ours, then this is not
        // a cross COI operation, and the URL can be nullified
        boolean b = localIdpURL.equals(xIdpURL);
        if (localIdpURL.equals(xIdpURL)) xIdpURL = null; 
        localIdentityStatement = (IdentityStatement)identityStatements.get(localIdpURL);


        // If no special idp url is defined, then this is a local COI service
        if (xIdpURL==null) { 
            guestIdentityStatement = null;
        } else {
            guestIdentityStatement = (IdentityStatement)identityStatements.get(xIdpURL);
        }
        
        // Do we have a valid identity statement? Renew also if the xCOI-IS is about to expire
//        if (localIdentityStatement == null)
//            System.out.println("My IS has not been issued yet");
//        else System.out.println("My IS expires in : " + 
//                (localIdentityStatement.getNotAfter().getTime()
//                -System.currentTimeMillis())/1000 + " seconds");
//        if (crossCOIIdentityStatement == null) System.out.println("There is no xCOI IS issued");
//        else System.out.println("xCOI IS expires in : " + 
//                (crossCOIIdentityStatement.getNotAfter().getTime()
//                -System.currentTimeMillis())/1000 + " seconds");
        
        if (localIdentityStatement==null || 
               localIdentityStatement.identityStatementExpired(MIN_MARGIN_TO_EXPIRATION) || // TODO Or a max skew variable?
               // NB Never set the parameter value of the following statement to a higher
               // number than the property value of IDENTITYSTATEMENT_XD_FUTURE_SLACK in the IdP
               (guestIdentityStatement != null && guestIdentityStatement.identityStatementExpired(0))) {
            try {  
                System.out.println("GETTING A New IS------------------");
                localIdentityStatement = getLocalIdentityStatement();
            } catch (GismoIdmException ge) {
                // Use the old IS if a new cannot be obtained.
                if (localIdentityStatement == null) throw ge;
                System.out.println("Cannot obtain new IS, has to cope with the old");
            }
        }    

        // Obtain a guest identity statement if necessary
        if ((xIdpURL!=null) && (guestIdentityStatement == null || guestIdentityStatement.identityStatementExpired(0))) {
            guestIdentityStatement = getGuestIdentityStatement(xIdpURL,localIdentityStatement);
        }
        
        IdentityStatement validationIS = null;
        if (xIdpURL != null) {
            tempIS = guestIdentityStatement.getGuestPart();
            clockCounter = (long)clockCounters.get(xIdpURL);
            validationIS = guestIdentityStatement.getCrossCOIPart();
        } else {
            tempIS = localIdentityStatement;
            clockCounter = (long)clockCounters.get(localIdpURL);
        }

        ServiceInvocationFactory sif = new ServiceInvocationFactory(protocolClass);
        ServiceInvocation sio = sif.createRequestInstance
                (request, tempIS, crOp, clockCounter);
        Object f=null;
        UUID requestUuid = sio.getNonce();
        
        f = goHTTP(serviceURL,sio);

        if (f instanceof ServiceInvocation) {
            sio = (ServiceInvocation)f;
            sio.validateResponse(requestUuid, validationIS, serviceID,clientReqs, crOp, allowExpiredIS);
            System.out.println(sio.getIdentityStatement());
        } else if (f instanceof Exception) throw (Exception)f;
        System.out.println(sio.getIdentityStatement());
        return sio.getReturns();
    }

    private static Object goHTTP(String url, Object o) throws IOException,ClassNotFoundException {
        URL serviceUrl = new URL(url);
        HttpURLConnection urlc = (HttpURLConnection)serviceUrl.openConnection();

        urlc.setRequestMethod("POST");
        urlc.setDoInput(true);
        urlc.setDoOutput(true);
        urlc.setRequestProperty("Content-Type", "application/x-javaobject");
        OutputStream os = urlc.getOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(os);
        oos.writeObject(o);
        oos.close();
        // Now wait for response
        // Error/timeout conditions throws a SocketException
        InputStream is = urlc.getInputStream();
        ObjectInputStream ois = new ObjectInputStream(is);
        Object o2 = ois.readObject();
        return o2;
    }
    
}
